package com.ktgj.common.core.utils.xml;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Namespace;
import org.dom4j.QName;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;

import com.ktgj.common.core.utils.helper.StringHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class XElement extends XNode{
	private final static Logger LOGGER = LoggerFactory.getLogger(XElement.class);
	private String name;
	private XAttributeCollection attributeCollection;
	private String value = "";
	private XElement parent;
	private XElementCollection children;

	public XElement getParent()
	{
		return parent;
	}

	public void setParent(XElement parent)
	{
		this.parent = parent;
		parent.children.add(this);
	}

	public String getName()
	{
		return name;
	}

	public void setName(String name)
	{
		this.name = name;
	}

	public XElement()
	{
		attributeCollection = new XAttributeCollection();
		children = new XElementCollection();
	}

	public XElement(String name, XNode... nodes)
	{
		this();
		this.name = name;
		for (XNode node : nodes)
		{

			if (node.getClass().equals(XDocument.class))
			{
				XDocument doc = (XDocument) node;
				XElement ele = doc.docElement;
				this.name = ele.name;
				for (XElement e : ele.children)
				{
					this.add(e);
				}

				for (XAttribute a : ele.attributeCollection)
				{
					this.attributeCollection.add(a);
				}

				continue;
			}

			if (node.getClass().equals(XElement.class))
			{
				this.children.add((XElement) node);
				continue;
			}

			if (node.getClass().equals(XAttribute.class))
			{
				this.attributeCollection.add((XAttribute) node);
			}
		}
	}

	public static XElement loadXml(String xml)
	{
		ByteArrayInputStream stream = null;
		try
		{
			stream = new ByteArrayInputStream(xml.getBytes());
			return load(stream);

		}
		finally
		{
			if (stream != null)
			{
				try
				{
					stream.close();
				}
				catch (IOException e)
				{
					LOGGER.error(e.getMessage());
				}
			}
		}
	}

	public static XElement load(String path)
	{
		File file = new File(path);
		if (!file.exists())
		{
			return null;
		}

		FileInputStream stream = null;
		try
		{
			try
			{
				stream = new FileInputStream(file);
				return load(stream);
			}
			catch (FileNotFoundException e)
			{
				LOGGER.error(e.getMessage());
				return null;
			}

		}
		finally
		{
			if (stream != null)
			{
				try
				{
					stream.close();
				}
				catch (IOException e)
				{
					LOGGER.error(e.getMessage());
				}
			}
		}
	}

	public static XElement load(InputStream stream)
	{
		SAXReader reader = new SAXReader();
		try
		{
			Document document = reader.read(stream);
			Element root = document.getRootElement();

			return readVisit(null, root);
		}
		catch (DocumentException e)
		{
			LOGGER.error(e.getMessage());
			return null;
		}
	}

	public static XElement parse(String xml)
	{
		try
		{
			return XElement.load(xml);
		}
		catch (Exception ex)
		{
			LOGGER.error(ex.getMessage());
			return null;
		}
	}

	private static XElement readVisit(XElement parent, Element element)
	{
		XElement xe = new XElement();
		xe.name = element.getName();
		xe.value = element.getText();
		xe.parent = parent;
		if (parent != null) parent.children.add(xe);
		@SuppressWarnings("unchecked") List<Attribute> attrs = element.attributes();
		if (attrs != null && attrs.size() > 0)
		{
			for (Attribute attr : attrs)
			{
				XAttribute xa = new XAttribute();

				xa.setNamespace(new XNamespace(attr.getNamespacePrefix(), attr.getNamespaceURI()));
				xa.setName(attr.getName());
				xa.setValue(attr.getValue());
				xe.attributeCollection.add(xa);
			}
		}

		@SuppressWarnings("unchecked") List<Element> childs = element.elements();
		for (Element child : childs)
		{
			readVisit(xe, child);
		}

		return xe;
	}

	public void save(String path)
	{
		Document document = DocumentHelper.createDocument();
		Element rootElement = flushVisit(null, this);
		document.setRootElement(rootElement);
		OutputFormat format = new OutputFormat("    ", true);
		try
		{
			XMLWriter writer = new XMLWriter(new FileOutputStream(path), format);
			writer.write(document);
			writer.flush();
		}
		catch (IOException e)
		{
			LOGGER.error(e.getMessage());
		}
	}

	private Element flushVisit(Element parent, XElement element)
	{
		Element e = null;
		if (parent == null)
		{
			e = DocumentHelper.createElement(element.name);
		}
		else
		{
			e = parent.addElement(element.name);
		}

		if (!StringHelper.isNullOrWhiteSpace(element.value)) e.setText(element.value);

		List<XAttribute> attrs = element.attributeCollection;
		if (attrs != null && attrs.size() > 0)
		{
			for (XAttribute attr : attrs)
			{
				Attribute xa = null;
				XNamespace ns = attr.getNamespace();
				if (ns != null && !ns.isEmpty())
				{
					QName qName = new QName(attr.getName(), new Namespace(ns.getPrefix(), ns.getUri()));
					xa = DocumentHelper.createAttribute(e, qName, attr.getValue());
				}
				else
				{
					xa = DocumentHelper.createAttribute(e, attr.getName(), attr.getValue());
				}

				e.add(xa);
			}
		}

		List<XElement> childs = element.elements();
		for (XElement child : childs)
		{
			flushVisit(e, child);
		}

		return e;
	}

	public void setAttributeValue(String name, String value)
	{
		this.attributeCollection.set(name, value);
	}

	public void add(XElement element)
	{
		element.parent = this;
		this.children.add(element);
	}

	public XElement element(String name)
	{
		return this.children.element(name);
	}

	public List<XElement> elements()
	{
		return this.children;
	}

	public List<XElement> elements(String name)
	{
		return this.children.elements(name);
	}

	public XAttribute attribute(String name)
	{
		return this.attributeCollection.get(name);
	}

	public XElement xPathSelectElement(String expression)
	{
		if (StringHelper.isNullOrEmpty(expression)) return null;
		XTokenPath path = new XTokenPath(expression);
		return path.match(this);
	}

	public XElement[] xPathSelectElements(String expression)
	{
		if (StringHelper.isNullOrEmpty(expression)) return null;
		XTokenPath path = new XTokenPath(expression);
		List<XElement> elements = path.matches(this);
		if (elements == null) return new XElement[0];
		XElement[] array = new XElement[elements.size()];
		for (int i = 0; i < elements.size(); i++)
		{
			array[i] = elements.get(i);
		}

		return array;
	}

	public void replaceWith(XElement e)
	{
		this.removeAll();
		this.children = e.children;
		this.attributeCollection = e.attributeCollection;
		this.value = e.value;
		this.name = e.name;
	}

	public void remove()
	{
		this.parent.children.remove(this);
	}

	public List<XAttribute> attributes()
	{
		return this.attributeCollection;
	}

	public String getValue()
	{
		return value;
	}

	public void setValue(String value)
	{
		this.value = value;
	}

	public void removeAll()
	{
		this.children.clear();
		this.attributeCollection.clear();
	}

	public String getAttributeValue(String name)
	{
		XAttribute attr = this.attribute(name);
		if (attr == null) return "";
		return attr.getValue();
	}

	private boolean containsAttribute(String name)
	{
		return this.attributeCollection.containsAttribute(name);
	}

	public String toString()
	{
		Document document = DocumentHelper.createDocument();
		Element rootElement = flushVisit(null, this);
		document.setRootElement(rootElement);
		OutputFormat format = new OutputFormat("    ", true);
		ByteArrayOutputStream stream = new ByteArrayOutputStream();
		try
		{
			XMLWriter writer = new XMLWriter(stream, format);
			writer.write(document);
			writer.flush();
			return new String(stream.toByteArray(), Charset.forName("utf-8"));
		}
		catch (IOException e)
		{
			LOGGER.error(e.getMessage());
		}
		finally
		{
			try
			{
				stream.close();
			}
			catch (IOException e)
			{
				LOGGER.error(e.getMessage());
			}
		}

		return "";

	}

	static class XPath
	{
		String path;
		XPathNode head;
		boolean anywhere;

		public XPath(String path)
		{
			if (path.startsWith("//"))
			{
				this.anywhere = true;
			}

			XPathNode prev = new XPathNode();
			this.head = prev;
			this.path = StringHelper.trimStart(path, "/");
			for (String node : path.split("/"))
			{
				if (StringHelper.isNullOrEmpty(node)) continue;
				String[] segs = node.split("\\[|\\]");
				XPathNode xn = new XPathNode();
				int from = 0;
				int index = node.indexOf("[");
				if (index == -1)
				{
					xn.name = node;
				}
				else
				{
					if (index > 0)
					{
						xn.name = segs[0];
						from = 1;
					}

					if (from < segs.length - 1)
					{
						xn.attrs = new ArrayList<>();
					}

					for (int i = from; i < segs.length; i++)
					{
						String seg = segs[i];
						if (StringHelper.isNullOrEmpty(seg)) continue;
						XPathAttribute att = new XPathAttribute();
						String[] kv = seg.split("=");
						if (kv.length > 0) att.name = kv[0].substring(1);
						if (kv.length > 1) att.value = kv[1];

						xn.attrs.add(att);
					}
				}
				prev.next = xn;
				prev = xn;
			}

			this.head = this.head.next;
		}

		public XElement match(XElement e)
		{
			if (!this.anywhere)
			{
				for (XElement ce : e.elements())
				{
					XElement xe = this.match(this.head, ce);
					if (xe != null) return xe;
				}

				return null;
			}

			return traverse(e);
		}

		public List<XElement> matches(XElement e)
		{

			List<XElement> elements = new ArrayList<>();
			if (!this.anywhere)
			{
				for (XElement ce : e.elements())
				{
					List<XElement> xes = this.matches(this.head, ce);
					if (xes != null)
					{
						elements.addAll(xes);
					}
				}

				return elements;
			}

			return traverses(e);
		}

		private XElement traverse(XElement e)
		{
			XElement xe = this.match(this.head, e);
			if (xe != null) return xe;

			if (e.children != null)
			{
				for (XElement child : e.children)
				{
					XElement xc = traverse(child);
					if (xc != null) return xc;
				}
			}

			return null;
		}

		private List<XElement> traverses(XElement e)
		{
			List<XElement> elements = new ArrayList<>();
			List<XElement> xes = this.matches(this.head, e);
			if (xes != null) elements.addAll(xes);

			if (e.children != null)
			{
				for (XElement child : e.children)
				{
					List<XElement> xcs = traverses(child);
					if (xcs != null)
					{
						elements.addAll(xcs);
					}
				}
			}

			return elements;
		}

		private XElement match(XPathNode cur, XElement e)
		{
			if (cur == null) return null;

			boolean success = valid(cur, e);
			if (!success) return null;
			cur = cur.next;

			if (cur != null)
			{
				for (XElement child : e.children)
				{
					XElement me = match(cur, child);
					if (me == null) continue;
					return me;
				}
			}

			return e;
		}

		private List<XElement> matches(XPathNode cur, XElement e)
		{
			if (cur == null) return null;

			boolean success = valid(cur, e);
			if (!success) return null;
			cur = cur.next;

			List<XElement> elements = new ArrayList<>();
			if (cur != null)
			{
				for (XElement child : e.children)
				{
					List<XElement> mes = matches(cur, child);
					if (mes == null) continue;
					elements.addAll(mes);
				}
			}
			else
			{
				elements.add(e);
			}

			return elements.size() > 0 ? elements : null;
		}

		public boolean valid(XPathNode cur, XElement e)
		{
			if (cur == null) return false;

			if (cur.name == null && cur.attrs == null) return false;
			if (cur.name != null)
			{
				if (!e.getName().equals(cur.name)) return false;
			}

			if (cur.attrs != null)
			{
				for (XPathAttribute att : cur.attrs)
				{
					if (!e.containsAttribute(att.name)) return false;
					if (att.value != null && !e.getAttributeValue(att.name).equals(att.value)) return false;
				}
			}

			return true;
		}
	}

	static class XPathNode
	{
		String name;
		List<XPathAttribute> attrs;
		XPathNode next;
	}

	static class XPathAttribute
	{
		String name;
		String value;
	}

	enum TokenType
	{
		ID, STRING, NUMBER, AND, OR, EQ, NEQ, GT, GE, LT, LE, LPAREN, RPAREN, SLASH, DSLASH, AT, LSQUARE, RSQUARE;
	}

	static class Token
	{
		int seq;
		TokenType type;
		String value;

		public Token()
		{
		}

		public Token(int seq, TokenType type, String value)
		{
			this.seq = seq;
			this.type = type;
			this.value = value;
		}
	}

	static class TokenLexer
	{
		static Pattern eq = Pattern.compile("^=");
		static Pattern neq = Pattern.compile("^\\<\\>");
		static Pattern le = Pattern.compile("^\\<=");
		static Pattern lt = Pattern.compile("^\\<");
		static Pattern ge = Pattern.compile("^\\>=");
		static Pattern gt = Pattern.compile("^\\>");
		static Pattern lparen = Pattern.compile("^\\(");
		static Pattern rparen = Pattern.compile("^\\)");
		static Pattern and = Pattern.compile("^[aA][nN][dD]");
		static Pattern or = Pattern.compile("^[oO][rR]");
		static Pattern str = Pattern.compile("^'(('')|[^'])*'");
		static Pattern number = Pattern.compile("^-?([0-9])*(\\.)?[0-9]+");
		static Pattern id = Pattern.compile("^[_a-zA-Z]*[_a-zA-Z0-9]+");
		static Pattern dslash = Pattern.compile("^//");
		static Pattern slash = Pattern.compile("^/");
		static Pattern at = Pattern.compile("^@");
		static Pattern lsquare = Pattern.compile("^\\[");
		static Pattern rsquare = Pattern.compile("^\\]");

		public List<Token> getTokens(String input)
		{
			List<Token> tokens = new Vector<Token>();
			if (StringHelper.isNullOrEmpty(input)) return tokens;
			input = StringHelper.trimStart(input, ".");

			int seq = 0;
			while (input.length() > 0)
			{
				input = input.trim();

				if (lsquare.matcher(input).find())
				{
					tokens.add(new Token(seq++, TokenType.LSQUARE, "["));
					input = input.substring(1);
					continue;
				}

				if (rsquare.matcher(input).find())
				{
					tokens.add(new Token(seq++, TokenType.RSQUARE, "]"));
					input = input.substring(1);
					continue;
				}

				if (at.matcher(input).find())
				{
					tokens.add(new Token(seq++, TokenType.AT, "@"));
					input = input.substring(1);
					continue;
				}

				if (dslash.matcher(input).find())
				{
					tokens.add(new Token(seq++, TokenType.DSLASH, "//"));
					input = input.substring(2);
					continue;
				}

				if (slash.matcher(input).find())
				{
					tokens.add(new Token(seq++, TokenType.SLASH, "/"));
					input = input.substring(1);
					continue;
				}

				if (neq.matcher(input).find())
				{
					tokens.add(new Token(seq++, TokenType.NEQ, "<>"));
					input = input.substring(2);
					continue;
				}

				if (le.matcher(input).find())
				{
					tokens.add(new Token(seq++, TokenType.LE, "<="));
					input = input.substring(2);
					continue;
				}

				if (ge.matcher(input).find())
				{
					tokens.add(new Token(seq++, TokenType.GE, ">="));
					input = input.substring(2);
					continue;
				}

				if (lt.matcher(input).find())
				{
					tokens.add(new Token(seq++, TokenType.LT, "<"));
					input = input.substring(1);
					continue;
				}

				if (gt.matcher(input).find())
				{
					tokens.add(new Token(seq++, TokenType.GT, ">"));
					input = input.substring(1);
					continue;
				}

				if (eq.matcher(input).find())
				{
					tokens.add(new Token(seq++, TokenType.EQ, "="));
					input = input.substring(1);
					continue;
				}

				if (lparen.matcher(input).find())
				{
					tokens.add(new Token(seq++, TokenType.LPAREN, "("));
					input = input.substring(1);
					continue;
				}

				if (rparen.matcher(input).find())
				{
					tokens.add(new Token(seq++, TokenType.RPAREN, ")"));
					input = input.substring(1);
					continue;
				}

				if (and.matcher(input).find())
				{
					tokens.add(new Token(seq++, TokenType.AND, "and"));
					input = input.substring(3);
					continue;
				}

				if (or.matcher(input).find())
				{
					tokens.add(new Token(seq++, TokenType.OR, "or"));
					input = input.substring(2);
					continue;
				}

				Matcher m = str.matcher(input);
				if (m.find())
				{
					tokens.add(new Token(seq++, TokenType.STRING, m.group().substring(1, m.group().length() - 1)));
					input = input.substring(m.group().length());
					continue;
				}

				m = number.matcher(input);
				if (m.find())
				{
					tokens.add(new Token(seq++, TokenType.NUMBER, m.group()));
					input = input.substring(m.group().length());
					continue;
				}

				m = id.matcher(input);
				if (m.find())
				{
					tokens.add(new Token(seq++, TokenType.ID, m.group()));
					input = input.substring(m.group().length());
					continue;
				}

				input = "";
			}

			return tokens;
		}
	}

	static class XPathTokenNode
	{
		public Token name;
		public List<XPathTokenAttribute> attrs;
		public XPathTokenNode next;
		public boolean anywhere = false;
	}

	static class XPathTokenAttribute
	{
		public Token name;
		public Token value;
	}

	static class XTokenPath
	{
		XPathTokenNode head;

		public XTokenPath(String path)
		{
			TokenLexer lexer = new TokenLexer();
			List<Token> list = lexer.getTokens(path);
			if (list.size() == 0) return;
			Token first = list.get(0);
			if (first.type != TokenType.DSLASH && first.type != TokenType.SLASH)
			{
				list.add(0, new Token(-1, TokenType.SLASH, ""));
			}

			Token[] tokens = new Token[list.size()];
			int k = 0;
			for (Token token : list)
				tokens[k++] = token;

			XPathTokenNode prev = null, node = null;
			for (int i = 0; i < tokens.length; i++)
			{
				Token token = tokens[i];
				if (token.type == TokenType.DSLASH || token.type == TokenType.SLASH)
				{
					node = new XPathTokenNode();
					if (prev == null)
						head = prev = node;
					else prev.next = node;

					node.anywhere = token.type == TokenType.DSLASH;
					i = composeNode(i + 1, node, tokens) - 1;

					prev = node;
				}
			}
		}

		private int composeNode(int from, XPathTokenNode node, Token[] tokens)
		{
			if (tokens.length - 1 < from) return from;
			XPathTokenAttribute attribute = null;
			node.attrs = new ArrayList<>();
			for (int i = from; i < tokens.length; i++)
			{
				Token token = tokens[i];
				switch (token.type)
				{
				case LPAREN:
				case RPAREN:
				case GT:
				case GE:
				case LE:
				case LT:
				case NEQ:
				case OR:
				case AND:
				case AT:
				case DSLASH:
				case EQ:
					break;

				case ID:
					if (attribute == null)
						node.name = token;
					else attribute.name = token;
					break;

				case LSQUARE:
					attribute = new XPathTokenAttribute();
					node.attrs.add(attribute);
					break;
				case RSQUARE:
					attribute = null;
					break;

				case SLASH:
					return i;
				case STRING:
				case NUMBER:
					attribute.value = token;
					break;
				default:
					break;
				}

			}

			return tokens.length;
		}

		public XElement match(XElement e)
		{
			if (!head.anywhere)
			{
				for (XElement ce : e.elements())
				{
					XElement xe = this.match(this.head, ce);
					if (xe != null) return xe;
				}

				return null;
			}

			return traverse(e);
		}

		public List<XElement> matches(XElement e)
		{

			List<XElement> elements = new ArrayList<>();
			if (!head.anywhere)
			{
				for (XElement ce : e.elements())
				{
					List<XElement> xes = this.matches(this.head, ce);
					if (xes != null)
					{
						elements.addAll(xes);
					}
				}

				return elements;
			}

			return traverses(e);
		}

		private XElement traverse(XElement e)
		{
			XElement xe = this.match(this.head, e);
			if (xe != null) return xe;

			if (e.children != null)
			{
				for (XElement child : e.children)
				{
					XElement xc = traverse(child);
					if (xc != null) return xc;
				}
			}

			return null;
		}

		private List<XElement> traverses(XElement e)
		{
			List<XElement> elements = new ArrayList<>();
			List<XElement> xes = this.matches(this.head, e);
			if (xes != null) elements.addAll(xes);

			if (e.children != null)
			{
				for (XElement child : e.children)
				{
					List<XElement> xcs = traverses(child);
					if (xcs != null)
					{
						elements.addAll(xcs);
					}
				}
			}

			return elements;
		}

		private XElement match(XPathTokenNode cur, XElement e)
		{
			if (cur == null) return null;

			boolean success = valid(cur, e);
			if (!success) return null;
			cur = cur.next;

			if (cur != null)
			{
				for (XElement child : e.children)
				{
					XElement me = match(cur, child);
					if (me == null) continue;
					return me;
				}

				return null;
			}

			return e;
		}

		private List<XElement> matches(XPathTokenNode cur, XElement e)
		{
			if (cur == null) return null;

			boolean success = valid(cur, e);
			if (!success) return null;
			cur = cur.next;

			List<XElement> elements = new ArrayList<>();
			if (cur != null)
			{
				for (XElement child : e.children)
				{
					List<XElement> mes = matches(cur, child);
					if (mes == null) continue;
					elements.addAll(mes);
				}
			}
			else
			{
				elements.add(e);
			}

			return elements.size() > 0 ? elements : null;
		}

		public boolean valid(XPathTokenNode cur, XElement e)
		{
			if (cur == null) return false;

			if (cur.name == null && cur.attrs == null) return false;
			if (cur.name != null)
			{
				if (!e.getName().equals(cur.name.value)) return false;
			}

			if (cur.attrs != null)
			{
				for (XPathTokenAttribute att : cur.attrs)
				{
					if (!e.containsAttribute(att.name.value)) return false;
					if (att.value != null && !e.getAttributeValue(att.name.value).equals(att.value.value)) return false;
				}
			}

			return true;
		}
	}

}
